記事一覧に戻る

PythonでOANDA APIの実行例を紹介

動機

OANDA APIのドキュメントを見て実装するのに難儀したため、シンプルな実行サンプルを共有したいと思った次第です。BOT制作に興味があるものの、ドキュメントを見て心折られた人たちの一助になれば幸いです。

OANDA APIのドキュメントの見方は前回記事にしていますが、実装例をあまり記載出来ませんでした。リベンジも兼ねて、解説を可能な限り排除し、サンプルをメインにした記事にしていきます。

主に、価格の変動から売買ポイントを判定して取引するようなBOT制作で使いそうなエンドポイントを中心に記載していきます。

さっそく行きましょうか。

実行環境

Python 3.9.6
requests 2.26.0

requestsはpythonでHTTPのリクエストを実行するときに よく使われるサードパーティのパッケージです。2022年12月時点で最新はv2.28.1です。最新版を入れてもサンプルには影響ありません。

入っていない場合は適宜インストールしてください。

前提

  • APIのトークン取得が完了していること。

  • 口座IDとトークンはjsonファイルから読み取る。

key.jsonの形式
{
  "id":"my-account-id",
  "key":"my-api-key"  
}
  • key.jsonは以下の関数で読み取る

def load_key(file_path):
  """
  Args:
    file_path (str): key.jsonのパス

  Returns:
    dict: jsonファイルのをdictにパースしたもの
  """
  with open(file_path,"r") as f:
    return json.load(f)
  • ヘッダは以下のように取得

def default_header(api_key):
  """
  Args:
    api_key (str): apiトークン

  Returns:
    dict: HTTPヘッダに使うdict
  """
  return {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + api_key
  }
  • liveのURLを使用。

# 本番 URL
LIVE_URL = "https://api-fxtrade.oanda.com"

現在価格(Pricing)

def pricing(conf, pair):
  """ 現在価格を取得

  Args:
    conf (dict): key.jsonを読み取ったもの
    pair (str): 通貨ペア。"USD_JPY","EUR_USD"等

  Returns:
    dict: レスポンスのbody部(json)をdictにパースしたもの
  """
  account_id = conf["id"]
  url = LIVE_URL + f"/v3/accounts/{account_id}/pricing"
  header = default_header(conf["key"])
  params = {"instruments": pair}
  res = requests.get(url, headers=header, params=params)
  return res.json()

import json
import requests
  
# 本番 URL
LIVE_URL = "https://api-fxtrade.oanda.com"


def load_key(file_path):
  """
  Args:
    file_path (str): key.jsonのパス

  Returns:
    dict: jsonファイルのをdictにパースしたもの
  """
  with open(file_path, "r") as f:
    return json.load(f)


def default_header(api_key):
  """
  Args:
    api_key (str): apiトークン

  Returns:
    dict: HTTPヘッダに使うdict
  """
  return {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + api_key
  }


def pricing(conf, pair):
  """ 現在価格を取得

  Args:
    conf (dict): key.jsonを読み取ったもの
    pair (str): 通貨ペア。"USD_JPY","EUR_USD"等

  Returns:
    dict: レスポンスのbody部(json)をdictにパースしたもの
  """
  account_id = conf["id"]
  url = LIVE_URL + f"/v3/accounts/{account_id}/pricing"
  header = default_header(conf["key"])
  params = {"instruments": pair}
  res = requests.get(url, headers=header, params=params)
  return res.json()
from pprint import pprint
# key.jsonを読み込む
conf = load_key("key.json")
# コンソールに出力
pprint(pricing(conf, "USD_JPY"))
{'prices': [{'asks': [{'liquidity': 3000000, 'price': '136.784'}],
       'bids': [{'liquidity': 3000000, 'price': '136.698'}],
       'closeoutAsk': '136.791',
       'closeoutBid': '136.691',
       'instrument': 'USD_JPY',
       'quoteHomeConversionFactors': {'negativeUnits': '1.00000000',
                      'positiveUnits': '1.00000000'},
       'status': 'non-tradeable',
       'time': '2022-12-16T21:58:01.745205015Z',
       'tradeable': False,
       'type': 'PRICE'}],
 'time': '2022-12-16T22:00:35.912643299Z'}

'price'が配列になっていますが、これは通貨ペアを複数指定できるためです。 以下のようにカンマ区切りで通貨ペアを指定すれば、その分配列で返ってきます。

pricing(conf,"USD_JPY,EUR_USD")

ロウソク足

def candles(conf, pair, gran, count=None, start=None, end=None):
  '''ロウソク足を取得する。
  startとendを両方指定する場合、countの指定は出来ない仕様。

  Args:
    conf  (dict): key.jsonを読み取ったもの
    pair  (str): 通貨ペア。"USD_JPY","EUR_USD"等
    gran  (str): 足の長さ。"S30","M5","H4","D"のように指定
    count (int): ロウソク足の個数。
    start (str|int): 最初のロウソク足のopen時間。
      "2022-12-15T21:55:00.000000000Z"の形式、もしくはunix timestampで指定。
    end   (str|int): 最後のロウソク足のopen時間
      "2022-12-15T21:55:00.000000000Z"の形式、もしくはunix timestampで指定。

  Returns
    dict: レスポンスのbody部(json)をdictにパースしたもの
  '''
  url = LIVE_URL + f"/v3/instruments/{pair}/candles"
  header = default_header(conf["key"])
  params = {
    "granularity": gran,  # str:"S30","M5","H4","D"のように指定
    "count": count,     # int:個数
    "from": start,    # str\int :最初のロウソク足のopen時間
    "to": end,       # str\int :最後のロウソク足のopen時間
  }
  res = requests.get(url, headers=header, params=params)
  return res.json()
import json
import requests
  
# 本番 URL
LIVE_URL = "https://api-fxtrade.oanda.com"


def load_key(file_path):
  """
  Args:
    file_path (str): key.jsonのパス

  Returns:
    dict: jsonファイルのをdictにパースしたもの
  """
  with open(file_path, "r") as f:
    return json.load(f)


def default_header(api_key):
  """
  Args:
    api_key (str): apiトークン

  Returns:
    dict: HTTPヘッダに使うdict
  """
  return {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + api_key
  }


def candles(conf, pair, gran, count=None, start=None, end=None):
  '''ロウソク足を取得する。
    startとendを両方指定する場合、countの指定は出来ない仕様。

  Args:
    conf  (dict): key.jsonを読み取ったもの
    pair  (str): 通貨ペア。"USD_JPY","EUR_USD"等
    gran  (str): 足の長さ。"S30","M5","H4","D"のように指定
    count (int): ロウソク足の個数。
    start (str|int): 最初のロウソク足のopen時間。
      "2022-12-15T21:55:00.000000000Z"の形式、もしくはunix timestampで指定。
    end   (str|int): 最後のロウソク足のopen時間
      "2022-12-15T21:55:00.000000000Z"の形式、もしくはunix timestampで指定。

  Returns
    dict: レスポンスのbody部(json)をdictにパースしたもの
  '''
  url = LIVE_URL + f"/v3/instruments/{pair}/candles"
  header = default_header(conf["key"])
  params = {
    "granularity": gran,  # str:"S30","M5","H4","D"のように指定
    "count": count,     # int:個数
    "from": start,    # str\int :最初のロウソク足のopen時間
    "to": end,       # str\int :最後のロウソク足のopen時間
  }
  res = requests.get(url, headers=header, params=params)
  return res.json()
from pprint import pprint

# key.jsonを読み込む
conf = load_key("key.json")

# 例1:直近の3個分のロウソク足を抽出
print("\n#[例1:実行結果]")
pprint(candles(conf, "USD_JPY", "H1", 3))

# 例2:指定した時間から、ロウソク足を5個分抽出する場合
print("\n#[例2:実行結果]")
pprint(candles(conf, "USD_JPY", "M5", 2, "2022-12-14T21:55:00.000000000Z"))

# 例3:指定した期間内のロウソク足を抽出
print("\n#[例3:実行結果]")
pprint(candles(conf, "USD_JPY", "M15", start="2022-12-14T21:00:00.000000000Z",
       end="2022-12-14T21:45:00.000000000Z"))
#[例1:実行結果]
{'candles': [{'complete': True,
              'mid': {'c': '136.552',
                      'h': '136.568',
                      'l': '136.382',
                      'o': '136.407'},
              'time': '2022-12-16T19:00:00.000000000Z',
              'volume': 2937},
             {'complete': True,
              'mid': {'c': '136.693',
                      'h': '136.699',
                      'l': '136.464',
                      'o': '136.552'},
              'time': '2022-12-16T20:00:00.000000000Z',
              'volume': 3075},
             {'complete': True,
              'mid': {'c': '136.741',
                      'h': '136.759',
                      'l': '136.598',
                      'o': '136.693'},
              'time': '2022-12-16T21:00:00.000000000Z',
              'volume': 2810}],
 'granularity': 'H1',
 'instrument': 'USD_JPY'}

#[例2:実行結果]
{'candles': [{'complete': True,
              'mid': {'c': '135.482',
                      'h': '135.500',
                      'l': '135.467',
                      'o': '135.480'},
              'time': '2022-12-14T21:55:00.000000000Z',
              'volume': 218},
             {'complete': True,
              'mid': {'c': '135.466',
                      'h': '135.476',
                      'l': '135.466',
                      'o': '135.467'},
              'time': '2022-12-14T22:00:00.000000000Z',
              'volume': 7}],
 'granularity': 'M5',
 'instrument': 'USD_JPY'}

#[例3:実行結果]
{'candles': [{'complete': True,
              'mid': {'c': '135.342',
                      'h': '135.357',
                      'l': '135.230',
                      'o': '135.260'},
              'time': '2022-12-14T21:00:00.000000000Z',
              'volume': 1879},
             {'complete': True,
              'mid': {'c': '135.445',
                      'h': '135.453',
                      'l': '135.330',
                      'o': '135.336'},
              'time': '2022-12-14T21:15:00.000000000Z',
              'volume': 1093},
             {'complete': True,
              'mid': {'c': '135.417',
                      'h': '135.450',
                      'l': '135.370',
                      'o': '135.442'},
              'time': '2022-12-14T21:30:00.000000000Z',
              'volume': 1154}],
 'granularity': 'M15',
 'instrument': 'USD_JPY'}

成行注文

注文は少し複雑です。リクエストのbody部を変えることで、成行注文、指値注文、ストップ注文等の実行が可能です。

ここでは成行注文を例にします。

def market_order(conf, pair, units, **kw):
  """成行注文
  買いの場合はunitsに正の値、売りの場合は負の値を指定。
  OANDAは原則両建て不可のため、保有ポジションと逆向きに注文すると
  原則決済注文となる。
  ※ 両建可能なアカウントでは未検証。

  新規注文例:
   - 新規売り -> units = -1000
   - 新規買い -> units = 1000

  決済注文例:
  - 買いポジ1000unitsを決済  -> units = -1000
  - 売りポジ-1000unitsを決済 -> units = 1000

  Args:
    conf (dict): key.jsonを読み取ったもの
    pair (str): 通貨ペア。"USD_JPY","EUR_USD"等
    units (int): 取引き量。売りの場合はマイナスで指定

    kw (dict): その他のオプション。timeInForce,positionFill等
      詳細は次を参照:
      https://developer.oanda.com/rest-live-v20/order-df/#OrderRequest
      のMarketOrderRequest

  Returns:
    dict: レスポンスのbody部(json)をdictにパースしたもの
  """
  account_id = conf["id"]
  url = LIVE_URL + f"/v3/accounts/{account_id}/orders"
  header = default_header(conf["key"])
  body = {"order": {
    "type": "MARKET",  # 成行きの場合は"MARKET"を指定
    "instrument": pair,
    "units": units,
    **kw,  # 他のオプションをマージ
  }}

  res = requests.post(url, headers=header, json=body)
  return res.json()
import json
import requests
  
# 本番 URL
LIVE_URL = "https://api-fxtrade.oanda.com"


def load_key(file_path):
  """
  Args:
    file_path (str): key.jsonのパス

  Returns:
    dict: jsonファイルのをdictにパースしたもの
  """
  with open(file_path, "r") as f:
    return json.load(f)


def default_header(api_key):
  """
  Args:
    api_key (str): apiトークン

  Returns:
    dict: HTTPヘッダに使うdict
  """
  return {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + api_key
  }


def market_order(conf, pair, units, **kw):
  """成行注文
  買いの場合はunitsに正の値、売りの場合は負の値を指定。
  OANDAは原則両建て不可のため、保有ポジションと逆向きに注文すると
  原則決済注文となる。
  ※ 両建可能なアカウントでは未検証。

  新規注文例:
   - 新規売り -> units = -1000
   - 新規買い -> units = 1000

  決済注文例:
  - 買いポジ1000unitsを決済  -> units = -1000
  - 売りポジ-1000unitsを決済 -> units = 1000

  Args:
    conf (dict): key.jsonを読み取ったもの
    pair (str): 通貨ペア。"USD_JPY","EUR_USD"等
    units (int): 取引き量。売りの場合はマイナスで指定

    kw (dict): その他のオプション。timeInForce,positionFill等
      詳細は次を参照:
      https://developer.oanda.com/rest-live-v20/order-df/#OrderRequest
      のMarketOrderRequest

  Returns:
    dict: レスポンスのbody部(json)をdictにパースしたもの
  """
  account_id = conf["id"]
  url = LIVE_URL + f"/v3/accounts/{account_id}/orders"
  header = default_header(conf["key"])
  body = {"order": {
    "type": "MARKET",  # 成行きの場合は"MARKET"を指定
    "instrument": pair,
    "units": units,
    **kw,  # 他のオプションをマージ
  }}

  res = requests.post(url, headers=header, json=body)
  return res.json()
from pprint import pprint

# key.jsonを読み込む
conf = load_key("key.json")

# 新規取引。EUR_USDを10units買い。
# 10units売りから入る場合、-10と指定する。
res = market_order(conf, "EUR_USD", 10)
print("[新規取引]---------------")
pprint(res)

# 決済取引。EUR_USDの買玉を10units決済。
# 売玉を10units決済する場合、-10と指定する。
res = market_order(conf, "EUR_USD", -10)
print("[決済取引]---------------")
pprint(res)

unitsの符号について

新規取引の場合、買いならの値、売りならの値を指定します。
決済取引の場合、買玉の決済ならの値、売玉の決済ならの値を指定します。

  • 実行結果

    情報量が多いですが、有益そうなものは決済時の orderFillTransaction['pl'] でしょうか。 取引で得られた利益が入っています。

    以下はあくまで取引が成功した場合のレスポンスです。土日に注文して取引が成立しなかった場合等、orderCancelTransactionのように別のレスポンスが返るケースもあります。

{'lastTransactionID': '1130',
 'orderCreateTransaction': {'accountID': '001-009-7411475-001',      
                            'batchID': '1129',
                            'id': '1129',
                            'instrument': 'EUR_USD',
                            'positionFill': 'DEFAULT',
                            'reason': 'CLIENT_ORDER',
                            'requestID': '25025005758673086',        
                            'time': '2022-12-19T14:10:18.929399615Z',
                            'timeInForce': 'FOK',
                            'type': 'MARKET_ORDER',
                            'units': '10',
                            'userID': 7411475},
 'orderFillTransaction': {'accountBalance': '195812.8070',
                          'accountID': '001-009-7411475-001',        
                          'baseFinancing': '0',
                          'batchID': '1129',
                          'commission': '0.0000',
                          'financing': '0.0000',
                          'fullPrice': {'asks': [{'liquidity': '3000000',
                                                  'price': '1.06082'}],
                                        'bids': [{'liquidity': '3000000',
                                                  'price': '1.06074'}],
                                        'closeoutAsk': '1.06084',
                                        'closeoutBid': '1.06071',
                                        'timestamp': '2022-12-19T14:10:16.991803070Z'},
                          'fullVWAP': '1.06082',
                          'gainQuoteHomeConversionFactor': '136.506440',
                          'guaranteedExecutionFee': '0.0000',
                          'halfSpreadCost': '0.0547',
                          'homeConversionFactors': {'gainBaseHome': {'factor': '144.802814'},
                                                    'gainQuoteHome': {'factor': '136.506440'},
                                                    'lossBaseHome': {'factor': '145.383186'},
                                                    'lossQuoteHome': {'factor': '137.053560'}},
                          'id': '1130',
                          'instrument': 'EUR_USD',
                          'lossQuoteHomeConversionFactor': '137.053560',
                          'orderID': '1129',
                          'pl': '0.0000',
                          'price': '1.06082',
                          'quoteGuaranteedExecutionFee': '0',
                          'quotePL': '0',
                          'reason': 'MARKET_ORDER',
                          'requestID': '25025005758673086',
                          'requestedUnits': '10',
                          'time': '2022-12-19T14:10:18.929399615Z',
                          'tradeOpened': {'guaranteedExecutionFee': '0.0000',
                                          'halfSpreadCost': '0.0547',
                                          'initialMarginRequired': '58.0372',
                                          'price': '1.06082',
                                          'quoteGuaranteedExecutionFee': '0',
                                          'tradeID': '1130',
                                          'units': '10'},
                          'type': 'ORDER_FILL',
                          'units': '10',
                          'userID': 7411475},
 'relatedTransactionIDs': ['1129', '1130']}

[決済取引]---------------
{'lastTransactionID': '1132',
 'orderCreateTransaction': {'accountID': '001-009-7411475-001',
                            'batchID': '1131',
                            'id': '1131',
                            'instrument': 'EUR_USD',
                            'positionFill': 'DEFAULT',
                            'reason': 'CLIENT_ORDER',
                            'requestID': '133111396816041108',
                            'time': '2022-12-19T14:10:19.276201820Z',
                            'timeInForce': 'FOK',
                            'type': 'MARKET_ORDER',
                            'units': '-10',
                            'userID': 7411475},
 'orderFillTransaction': {'accountBalance': '195812.6974',
                          'accountID': '001-009-7411475-001',
                          'baseFinancing': '0.00000000000000',
                          'batchID': '1131',
                          'commission': '0.0000',
                          'financing': '0.0000',
                          'fullPrice': {'asks': [{'liquidity': '2999990',
                                                  'price': '1.06082'}],
                                        'bids': [{'liquidity': '3000000',
                                                  'price': '1.06074'}],
                                        'closeoutAsk': '1.06084',
                                        'closeoutBid': '1.06071',
                                        'timestamp': '2022-12-19T14:10:16.991803070Z'},
                          'fullVWAP': '1.06074',
                          'gainQuoteHomeConversionFactor': '136.506440',
                          'guaranteedExecutionFee': '0.0000',
                          'halfSpreadCost': '0.0547',
                          'homeConversionFactors': {'gainBaseHome': {'factor': '144.802814'},
                                                    'gainQuoteHome': {'factor': '136.506440'},
                                                    'lossBaseHome': {'factor': '145.383186'},
                                                    'lossQuoteHome': {'factor': '137.053560'}},
                          'id': '1132',
                          'instrument': 'EUR_USD',
                          'lossQuoteHomeConversionFactor': '137.053560',
                          'orderID': '1131',
                          'pl': '-0.1096',
                          'price': '1.06074',
                          'quoteGuaranteedExecutionFee': '0',
                          'quotePL': '-0.00080',
                          'reason': 'MARKET_ORDER',
                          'requestID': '133111396816041108',
                          'requestedUnits': '-10',
                          'time': '2022-12-19T14:10:19.276201820Z',
                          'tradesClosed': [{'baseFinancing': '0.00000000000000',
                                            'financing': '0.0000',
                                            'guaranteedExecutionFee': '0.0000',
                                            'halfSpreadCost': '0.0547',
                                            'price': '1.06074',
                                            'quoteGuaranteedExecutionFee': '0',
                                            'realizedPL': '-0.1096',
                                            'tradeID': '1130',
                                            'units': '-10'}],
                          'type': 'ORDER_FILL',
                          'units': '-10',
                          'userID': 7411475},
 'relatedTransactionIDs': ['1131', '1132']}

最後に

今回は、自動取引BOT作成に重要な、現在価格、ロウソク足の取得、そして成行注文のサンプルを掲載しました。 少し冗長な記載も多かったかもしれませんが、、、。引き続き、保有ポジションの取得や、評価額の確認方法等、 他のサンプルも第二弾として作成したいと思います。

ご要望あれば、私、全力君のtwitterもしくはこのサイトの掲示板でお気軽に連絡ください。

記事一覧に戻る