Help us understand the problem. What is going on with this article?

機械学習でFX:Oanda APIを使ってPythonから自動売買する

機械学習やDeep Learningで日々腕を磨いている皆さん、一度は競馬やFXの予測で儲けてみたいと思った事はありませんか?
競馬やFXを機械学習やDeep Learningのモデルで予測してみることは非常にいい勉強・経験になるのでオススメです。
この投稿ではPyhonでFXの自動売買を動かすのに最適なOanda APIについて紹介したいと思います。

Oanda APIとは

Oandaという会社が提供しているFXの自動売買のためのAPIです。
個人でも利用できるAPIを提供している会社は少ないのですが、OandaはpythonからAPIを利用するためのパッケージ(oandapyV20)も存在しており、pythonユーザにとっては非常に使い易いサービスとなっています。

APIを利用するには、Oanda社で口座を登録して、APIのためのトークンを発行してもらう必要があります。
デモ環境であれば、以下のページから15分もあれば口座開設してトークン発行可能です。

https://www.oanda.jp/

なお、デモ環境では、架空のお金で本番取引と同じように取引を行う事ができ、自動売買のモデルが本当に利益を出せるのかシミュレーションする事が出来ます。
精度の悪いモデルや自動売買の処理のバグで自分のお金を溶かしてしまうリスクを軽減する事ができますね。

公式ドキュメントは以下を参照ください。

oandaV20公式ドキュメント
oandaV20Pyhonライブラリのドキュメント

また、Oanda APIの利用にあたっては、以下のページを参考にさせていただきました。
大量の為替過去データをFX APIから取得する方法(機械学習用)

OandapyV20のインストールと利用の準備

インストールはpipから可能です。

$ pip install oandapyV20

インストールしたらpythonでimportすれば利用できるようになります。

from oandapyV20 import API
from oandapyV20.exceptions import V20Error
from oandapyV20.endpoints.pricing import PricingStream
import oandapyV20.endpoints.orders as orders
import oandapyV20.endpoints.instruments as instruments

Oanda.jpでデモアカウントを作成して発行されたアカウントIDとアクセストークンを使って、接続を確立します。
デモ環境を利用するため、environmentには'pracice'を指定してください。

#自分のアカウント、トークンをセット
accountID = "101-999-9999999999-999"
access_token = 'xxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxx'

api = API(access_token=access_token, environment="practice")

Oanda APIの利用

Oanda APIを使った情報の取得

FXを機械学習するためには、分析のためのデータの取得が欠かせません。
Oanda APIを使って必要なデータを取得してみましょう。

取引可能な通貨情報の取得

取引対象の通貨は、各APIでInstrumentとして指定します。各通貨ペアのAPIでの名前を押させて置く必要があります。
主要な通貨ペアのコードは以下の通りです。

  • USD_JPY
  • EUR_JPY
  • EUR_USD

AccountInstrumentsクラスを使って、取引可能な通貨の情報を取得します。
paramsで対象の通貨ペアを指定してリクエストします。

params = { "instruments": "EUR_USD,EUR_JPY,USD_JPY" }
import oandapyV20.endpoints.accounts as accounts
r = accounts.AccountInstruments(accountID=accountID, params=params)
api.request(r)

JSON形式で結果が返ってきました。
各通貨ペアの最小取引単位やスプリットレートなどが確認できます。

{'instruments': [{'name': 'EUR_USD',
   'type': 'CURRENCY',
   'displayName': 'EUR/USD',
   'pipLocation': -4,
   'displayPrecision': 5,
   'tradeUnitsPrecision': 0,
   'minimumTradeSize': '1',
   'maximumTrailingStopDistance': '1.00000',
   'minimumTrailingStopDistance': '0.00050',
   'maximumPositionSize': '0',
   'maximumOrderUnits': '100000000',
   'marginRate': '0.04',
   'tags': [{'type': 'ASSET_CLASS', 'name': 'CURRENCY'}]},
  {'name': 'EUR_JPY',
   'type': 'CURRENCY',
   'displayName': 'EUR/JPY',
   'pipLocation': -2,
   'displayPrecision': 3,
   'tradeUnitsPrecision': 0,
   'minimumTradeSize': '1',
   'maximumTrailingStopDistance': '100.000',
   'minimumTrailingStopDistance': '0.050',
   'maximumPositionSize': '0',
   'maximumOrderUnits': '100000000',
   'marginRate': '0.04',
   'tags': [{'type': 'ASSET_CLASS', 'name': 'CURRENCY'}]},
  {'name': 'USD_JPY',
   'type': 'CURRENCY',
   'displayName': 'USD/JPY',
   'pipLocation': -2,
   'displayPrecision': 3,
   'tradeUnitsPrecision': 0,
   'minimumTradeSize': '1',
   'maximumTrailingStopDistance': '100.000',
   'minimumTrailingStopDistance': '0.050',
   'maximumPositionSize': '0',
   'maximumOrderUnits': '100000000',
   'marginRate': '0.04',
   'tags': [{'type': 'ASSET_CLASS', 'name': 'CURRENCY'}]}],
 'lastTransactionID': '17'}

口座情報の取得

AccountSummaryで残高やポジション数などの口座のサマリが取得できます。

import oandapyV20.endpoints.accounts as accounts
r = accounts.AccountSummary(accountID)
api.request(r)

JSON形式で結果が返ってきます。
主要なところでは、balanceが現在の残高、plが口座の全期間での利益(+)または損失(ー)、openPositionCountが現在のOpenなポジションの数、positionValueが現在のポジションとなります。
個々の項目の詳細はこちらに記載があります。

{'account': {'guaranteedStopLossOrderMode': 'DISABLED',
  'hedgingEnabled': False,
  'id': 'xxxxxxxxxxxxxxxxxxxxxxx',
  'createdTime': '2019-xx-xxT01:40:16.169955147Z',
  'currency': 'JPY',
  'createdByUserID': xxxxxxx,
  'alias': 'Primary',
  'marginRate': '0.04',
  'lastTransactionID': '17',
  'balance': '2999521.9000',
  'openTradeCount': 2,
  'openPositionCount': 2,
  'pendingOrderCount': 0,
  'pl': '-478.1000',
  'resettablePL': '-478.1000',
  'resettablePLTime': '0',
  'financing': '0.0000',
  'commission': '0.0000',
  'dividend': '0',
  'guaranteedExecutionFees': '0.0000',
  'unrealizedPL': '-1816.8395',
  'NAV': '2997705.0605',
  'marginUsed': '46824.5600',
  'marginAvailable': '2950880.5005',
  'positionValue': '1170614.0000',
  'marginCloseoutUnrealizedPL': '-1794.7328',
  'marginCloseoutNAV': '2997727.1672',
  'marginCloseoutMarginUsed': '46824.5600',
  'marginCloseoutPositionValue': '1170614.0000',
  'marginCloseoutPercent': '0.01562',
  'withdrawalLimit': '2950880.5005'},
 'lastTransactionID': '17'}

価格データの取得

各通貨ペアの価格データを取得するには、InstrumentsCandlesクラスを利用します。
引数として、対象の通貨ペア(ここではUSD/JPY)と直近5件、5分足(M5)を指定しました。

import oandapyV20.endpoints.instruments as instruments
params = {
  "count": 5,
  "granularity": "M5"
}
r = instruments.InstrumentsCandles(instrument="USD_JPY", params=params)
api.request(r)

JSONの戻り値を確認しましょう。
candleデータとして取引量(volume)、時間(time)、開始(o:open)、高値(h:high)、底値(l:low)、終値(c:clole)の価格が戻って来ています。

{'instrument': 'USD_JPY',
 'granularity': 'M5',
 'candles': [{'complete': True,
   'volume': 68,
   'time': '2019-08-13T07:25:00.000000000Z',
   'mid': {'o': '105.314', 'h': '105.314', 'l': '105.285', 'c': '105.302'}},
  {'complete': True,
   'volume': 81,
   'time': '2019-08-13T07:30:00.000000000Z',
   'mid': {'o': '105.300', 'h': '105.325', 'l': '105.297', 'c': '105.308'}},
  {'complete': True,
   'volume': 68,
   'time': '2019-08-13T07:35:00.000000000Z',
   'mid': {'o': '105.306', 'h': '105.306', 'l': '105.268', 'c': '105.280'}},
  {'complete': True,
   'volume': 67,
   'time': '2019-08-13T07:40:00.000000000Z',
   'mid': {'o': '105.278', 'h': '105.284', 'l': '105.255', 'c': '105.265'}},
  {'complete': False,
   'volume': 43,
   'time': '2019-08-13T07:45:00.000000000Z',
   'mid': {'o': '105.268', 'h': '105.273', 'l': '105.252', 'c': '105.256'}}]}

取得する価格の時間足ですが、次のものが指定できます。
かなりいろいろな条件でデータが取得できますが、1回のデータ取得で最大5000件までという制約があります。

  • S5/S10/S15/S30 : それぞれ5秒足、10秒足、15秒足、30秒足
  • M1/M2/M4/M5/M10/M15/M30 : それぞれ1分足、2分足、4分足、5分足、10分足、15分足、30分足
  • H1/H2/H3/H4/H6/H12 : それぞれ1時間足、2時間足、3時間足、4時間足、6時間足、8時間足、12時間足
  • D : 日足
  • W : 週足
  • M : 月足

注文処理

Oanda APIではかなり複雑な注文方法にも対応しています。
オーダーは、orders.OrderCreateでオーダーを作成してから、APIにリクエストすると実行されます。オーダー方法の詳細はdataパラメータで指定しています。

ここではpriceで注文金額、有効期間としてGTC(Good unTill Cancelled : キャンセルまで有効)、対象の通過ペア(EUR/USD)、数量単位として100を指定しています。
数量(units)の+はLongの買い、-はShortの売りを意味しています。
stopLossOnFillでは、stopLossがトリガーされる金額と、有効期間としてGTC(Good unTill Cancelled :キャンセルまで有効)を指定しています。

import oandapyV20.endpoints.orders as orders
data =  {
    "order": {
        "price": "1.2",
        "stopLossOnFill": {
            "timeInForce": "GTC",
            "price": "1.22"
        },
        "timeInForce": "GTC",
        "instrument": "EUR_USD",
        "units": "-100",
        "type": "LIMIT",
        "positionFill": "DEFAULT"
    }    
}
r = orders.OrderCreate(accountID, data=data)
api.request(r)

約定したらその結果がJSONのレスポンスとして戻って来ます。

{'orderCreateTransaction': {'type': 'LIMIT_ORDER',
  'instrument': 'EUR_USD',
  'units': '-100',
  'price': '1.20000',
  'timeInForce': 'GTC',
  'triggerCondition': 'DEFAULT',
  'partialFill': 'DEFAULT',
  'positionFill': 'DEFAULT',
  'stopLossOnFill': {'price': '1.22000', 'timeInForce': 'GTC'},
  'reason': 'CLIENT_ORDER',
  'id': '18',
  'accountID': 'xxxxxxxxxxxxxxxxxxxxx',
  'userID': xxxxxxxx,
  'batchID': '18',
  'requestID': '42595754149026610',
  'time': '2019-08-13T08:23:07.599090557Z'},
 'relatedTransactionIDs': ['18'],
 'lastTransactionID': '18'}

オーダー方法としては次のようなオーダーに対応しているようです。
- MarketOrder : 成行
- FixedPriceOrder : 指値
- LimitOrder : 閾値より良ければ約定
- StopOrder : 閾値より悪ければ約定

他にもMarketIfTouchedOrder, TakeProfitOrder, StopLossOrder, TrailingStopLossOrderなどあるようですが、使い分けはよく分かっていません。

約定したオーダーのポジションを確認したりクローズすることができます。
OpenPositionを利用して現在のポジションの一覧を取得できます。買い、もしくは売りのどちらかのポジションを持っていればその状況が出力されます。

import oandapyV20.endpoints.positions as positions
r = positions.OpenPositions(accountID=accountID)
api.request(r)

出力結果には次の情報が含まれています。
- Long/Shortそれぞれの損益と、totalの損益
- unrealizedPL : 現時点で未確定の損益
- resettablePL : 確定済みの損益です。

{'positions': [{'instrument': 'USD_JPY',
   'long': {'units': '10000',
    'averagePrice': '105.462',
    'pl': '0.0000',
    'resettablePL': '0.0000',
    'financing': '0.0000',
    'dividend': '0.0000',
    'guaranteedExecutionFees': '0.0000',
    'tradeIDs': ['15'],
    'unrealizedPL': '-1590.0000'},
   'short': {'units': '0',
    'pl': '-470.0000',
    'resettablePL': '-470.0000',
    'financing': '0.0000',
    'dividend': '0.0000',
    'guaranteedExecutionFees': '0.0000',
    'unrealizedPL': '0.0000'},
   'pl': '-470.0000',
   'resettablePL': '-470.0000',
   'financing': '0.0000',
   'commission': '0.0000',
   'dividend': '0.0000',
   'guaranteedExecutionFees': '0.0000',
   'unrealizedPL': '-1590.0000',
   'marginUsed': '42122.0000'},
  {'instrument': 'EUR_USD',
   'long': {'units': '1000',
    'averagePrice': '1.11884',
    'pl': '0.0000',
    'resettablePL': '0.0000',
    'financing': '0.0000',
    'dividend': '0.0000',
    'guaranteedExecutionFees': '0.0000',
    'tradeIDs': ['7'],
    'unrealizedPL': '34.7487'},
   'short': {'units': '0',
    'pl': '0.0000',
    'resettablePL': '0.0000',
    'financing': '0.0000',
    'dividend': '0.0000',
    'guaranteedExecutionFees': '0.0000',
    'unrealizedPL': '0.0000'},
   'pl': '0.0000',
   'resettablePL': '0.0000',
   'financing': '0.0000',
   'commission': '0.0000',
   'dividend': '0.0000',
   'guaranteedExecutionFees': '0.0000',
   'unrealizedPL': '34.7487',
   'marginUsed': '4714.2400'}],
 'lastTransactionID': '18'}

USD/JPYの10000のlongのポジションのうち、5000をクローズする場合の処理は次のようになります。

import oandapyV20.endpoints.positions as positions
data = {
  "longUnits": "5000"
}
r = positions.PositionClose(accountID=accountID,
                             instrument='USD_JPY',
                             data=data)
api.request(r)

結果として5000の売りが発生した事を示す結果が戻ってきました。

{'longOrderCreateTransaction': {'type': 'MARKET_ORDER',
  'instrument': 'USD_JPY',
  'units': '-5000',
  'timeInForce': 'FOK',
  'positionFill': 'REDUCE_ONLY',
  'reason': 'POSITION_CLOSEOUT',
  'longPositionCloseout': {'instrument': 'USD_JPY', 'units': '5000'},
  'id': '19',
  'time': '2019-08-13T09:05:59.038645609Z'},
##以下省略###

機械学習のための価格データ取得

ここまでで、Oanda APIの基本的な使い方が理解できました。
続いて、機械学習のトレーニングで利用するための価格データをまとめて取得し、PandasのData Frameに格納するところまで実装してみましょう。

2016年1月から2019年9月までの10分足データをOanda APIを使って取得してみます。

10分足データの取得

まずは必要なライブラリをインポートし、アクセス用のクライアントを設定します。
accountIDaccess_tokenは各自のコードで置き換えてください。

from oandapyV20 import API
from oandapyV20.exceptions import V20Error
from oandapyV20.endpoints.pricing import PricingStream
import oandapyV20.endpoints.orders as orders
import oandapyV20.endpoints.instruments as instruments

import json
import datetime
import pandas as pd
from dateutil.relativedelta import relativedelta

accountID = "xxxx"
access_token = 'xxxxx'
api = API(access_token=access_token, environment="practice")

Oanda APIで価格データを取得するには、一度に5,000件までという制約があります。
10分足だと1日分が6 * 24 = 144件、約34日分が5000件となります。
そのため、1ヶ月分づつをループ処理で回しながら取得していくのが良いでしょう。

year_months =[
    [2016, 1], [2016, 2], [2016, 3], [2016, 4], [2016, 5], [2016, 6], [2016, 7], [2016, 8], [2016, 9], [2016, 10], [2016, 11], [2016, 12],
    [2017, 1], [2017, 2], [2017, 3], [2017, 4], [2017, 5], [2017, 6], [2017, 7], [2017, 8], [2017, 9], [2017, 10], [2017, 11], [2017, 12],
    [2018, 1], [2018, 2], [2018, 3], [2018, 4], [2018, 5], [2018, 6], [2018, 7], [2018, 8], [2018, 9], [2018, 10], [2018, 11], [2018, 12],
    [2019, 1], [2019, 2], [2019, 3], [2019, 4], [2019, 5], [2019, 6], [2019, 7],
]

candleデータを取得する処理をメソッドにしました。
fromとtoをdatetime型で受けて、Oanda APIでcandleデータを取得します。
datetimeのisoformat()で、Oanda APIで利用できるisoフォーマットに変換する必要があります。

# Oandaからcandleデータを取得する。
def getCandleDataFromOanda(instrument, api, date_from, date_to, granularity):
    params = {
        "from": date_from.isoformat(),
        "to": date_to.isoformat(),
        "granularity": granularity,
    }
    r = instruments.InstrumentsCandles(instrument=instrument, params=params)
    return api.request(r)

Oanda APIからの戻り値はJSON形式なので、それをpythonの配列に変換するメソッドを用意しました。
戻り値の日付をisoフォーマットからdatatime形式へ変換していますが、小数点以下があるとうまく変換できなかったので、秒までの頭19桁で切ったものをdatetimeに変換する事にしました。

def oandaJsonToPythonList(JSONRes):
    data = []
    for res in JSONRes['candles']:
        data.append( [
            datetime.datetime.fromisoformat(res['time'][:19]),
            res['volume'],
            res['mid']['o'],
            res['mid']['h'],
            res['mid']['l'],
            res['mid']['c'],
            ])
    return data

ヘルパーメソッドが揃ったところで、年月でループしながらデータを取得していきます
上記で定義したメソッドを呼び出して配列に格納していくだけです。
M10は10分データを意味します。

all_data = []
# year, monthでループ
for year, month in year_months:
    date_from = datetime.datetime(year, month, 1)
    date_to = date_from + relativedelta(months=+1, day=1)

    ret = getCandleDataFromOanda("USD_JPY", api, date_from, date_to, "M10")
    month_data = oandaJsonToPythonList(ret)

    all_data.extend(month_data)

Pandas DataFrameへの変換

取得したPython配列データを扱いやすいようにpandasのDataFrameへ変換します。

# pandas DataFrameへ変換
df = pd.DataFrame(all_data)
df.columns = ['Datetime', 'Volume', 'Open', 'High', 'Low', 'Close']
df = df.set_index('Datetime')

これで2016年1月から2019年9月までの10分足データが取得できました。
7/30までのデータをToに指定して取得していますが、Oanda APIの仕様上、日付指定では翌日3:50までのデータが抽出されるようです。いったん気にしないでおきます。

[5] df.shape
(133486, 5)

[6] df.head(5)
Volume  Open    High    Low Close
Datetime                    
2016-01-03 22:00:00 162 120.195 120.235 120.194 120.227
2016-01-03 22:10:00 208 120.226 120.253 120.209 120.236
2016-01-03 22:20:00 333 120.235 120.283 120.233 120.274
2016-01-03 22:30:00 359 120.274 120.304 120.268 120.286
2016-01-03 22:40:00 242 120.288 120.330 120.277 120.313

[7] df.tail(5)
Volume  Open    High    Low Close
Datetime                    
2019-08-01 03:10:00 62  109.152 109.187 109.152 109.182
2019-08-01 03:20:00 70  109.180 109.212 109.172 109.204
2019-08-01 03:30:00 52  109.207 109.210 109.185 109.195
2019-08-01 03:40:00 73  109.197 109.232 109.192 109.216
2019-08-01 03:50:00 71  109.218 109.252 109.218 109.239

時間はUTCですが、日本時間への変換は行わず、UTCのまま扱っていくことにします。
いつでも再利用できるように、csv形式で保存しておきます。
pandasのto_csvread_csvで簡単に扱うことができます。

# CSVファイルへの出力
df.to_csv('./USD_JPY_201601-201908_M10.csv')

# CSVファイルの読み取り
df = pd.read_csv('./USD_JPY_201601-201908_M10.csv', index_col='Datetime')

これで機械学習で学習させるためのデータが手に入りました。13万件のデータは機械学習で利用するには最小限のデータですので、必要に応じて期間を伸ばしたり分足等の短い間隔のデータを取得すればよいでしょう。

次回は機械学習で予測モデルを作る前に、もう一つ重要なツールであるBacktesting.pyを紹介したいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした