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)
-
現在価格のドキュメント。上から2つ目。
-
endpoint:/v3/accounts/{accountID}/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")
ロウソク足
-
ロウソク足のドキュメント。上から1つ目。
-
endpoint:/v3/instruments/{instrument}/candles
-
サンプル・スクリプト(該当部分のみ)
現在価格とは異なり、複数通貨ペアの指定は出来ない点に注意。
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部を変えることで、成行注文、指値注文、ストップ注文等の実行が可能です。
ここでは成行注文を例にします。
-
注文のドキュメント。上から1つ目。
-
endpoint:/v3/accounts/{accountID}/orders
-
参考:body部に指定可能なデータ。成行はMarketOrderRequestの箇所。
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もしくはこのサイトの掲示板でお気軽に連絡ください。