OANDA (oandapyV20)で、ゴールデンクロスで買い、デッドクロスで売るBOTを作ってみます。
下記の要素は、プログラム中で指定できるようにします。
- 単位期間 (年月日等)
- 移動平均を取る期間
- 対象とする通貨
もくじ
前提
結論
下記のプログラムのOANDA API認証部分 (accountID, access_token
) とLINE NOTIFY認証部分 (line_notify_token
)を自分のものに合わせた上で貼り付ければ、動きます。日足、5日と20日のゴールデンクロス・デットクロスを見るのが初期値です。
下記のプログラムを走らせると本当に売り買いを始めるので、実行の際は注意してください。global
変数が入り混じっていたり、プログラム途中でimport
したりしているプログラムではあるので、気になる方はご自身で修正をお願いします。
from oandapyV20 import API accountID = '001-007-2467777-007' access_token = '1k90d90od937c2dkj959c3a006c3dkjd-494f03267cd6cdkf9180098738edfk09' api = API(access_token=access_token, environment='live') period_base = 'D' period_short = 5 period_long = 20 instrument = 'USD_JPY' amount_jpy = 100000 import pandas as pd df = pd.DataFrame(columns=['Time', 'Open', 'High', 'Low', 'Close', 'Volume']) import oandapyV20.endpoints.instruments as instruments params = { "count": period_long+1, "granularity": period_base } r = instruments.InstrumentsCandles(instrument=instrument, params=params) api.request(r) res = r.response for i, c in enumerate(res['candles']): df.loc[i] = [c['time'].split('.')[0], c['mid']['o'], c['mid']['h'], c['mid']['l'], c['mid']['c'], c['volume']] # print(df) df_lperiod = df['Close'].rolling(period_long).mean() df_speriod = df['Close'].rolling(period_short).mean() # print(df_lperiod) # print(df_speriod) import oandapyV20.endpoints.positions as positions def all_position_close (): global accountID r = positions.OpenPositions(accountID=accountID) api.request(r) for p in r.response['positions']: inst = p['instrument'] long_ = 0 if p['long']['units'] == 0 else p['long']['units'] short_ = 0 if p['short']['units'] == 0 else p['short']['units'] data ={} if long_ != 0 or short_ != 0: if long_ != 0: data = {'longUnits': 'ALL'} elif short_ != 0: data = {'shortUnits': 'ALL'} r = positions.PositionClose(accountID=accountID, instrument=inst, data=data) api.request(r) import oandapyV20.endpoints.pricing as pricing def jpy_convert(amount_jpy, instrument): global accountID if instrument[-3:] != 'JPY': print('"jpy_convert" can convert only instrument of "XXX_JPY", cannot covert', instrument) exit() params = {'instruments': instrument} r = pricing.PricingInfo(accountID=accountID, params=params) rv = api.request(r) ab = (float(rv['prices'][0]['closeoutBid']) +float(rv['prices'][0]['closeoutAsk'])) / 2 provisional_unit = int(abs(amount_jpy) / ab) sign = 1 if amount_jpy > 0 else -1 return sign * provisional_unit import oandapyV20.endpoints.orders as orders def market_order (amount_jpy): global instrument, accountID if instrument[-3:] != 'JPY': return 'This function is only for instruments of XXX_JPY' unit = jpy_convert(amount_jpy, instrument) data = {'order': {'instrument': instrument, 'units': str(unit), 'type': 'MARKET' }} r = orders.OrderCreate(accountID, data=data) api.request(r) def golden_cross (): global amount_jpy all_position_close() market_order(amount_jpy) def dead_cross (): global amount_jpy all_position_close() market_order(-amount_jpy) import requests def line_notify(message): line_notify_token = 'K90oaljdkl98KD0p9K9pdDO0onLQq5GcMjnDKOLtTGJ' line_notify_api = 'https://notify-api.line.me/api/notify' payload = {'message': message} headers = {'Authorization': 'Bearer ' + line_notify_token} requests.post(line_notify_api, data=payload, headers=headers) print(message) if df_speriod.iloc[-2] < df_lperiod.iloc[-2] and df_speriod.iloc[-1] > df_lperiod.iloc[-1]: golden_cross() line_notify('OANDA BOT: GOLDEN CROSS') elif df_speriod.iloc[-2] > df_lperiod.iloc[-2] and df_speriod.iloc[-1] < df_lperiod.iloc[-1]: dead_cross() line_notify('OANDA BOT: DEAD CROSS') else: line_notify('OANDA BOT: NO ACTION')
プログラムの説明 (文芸的プログラミング)
oandapyV20の必須パラメータ指定
まず、oandapyV20を利用するために必要な必須パラメータを指定しておきます。
from oandapyV20 import API accountID = '001-007-2467777-007' access_token = '1k90d90od937c2dkj959c3a006c3dkjd-494f03267cd6cdkf9180098738edfk09' api = API(access_token=access_token, environment='live')
単位期間等のパラメータ指定
単位期間等のパラメータを指定します。指定可能な通貨ペアについては、こちらの過去記事です。
period_base
: 単位期間 (5分足、日足、等の指定)period_short
: 移動平均を取る期間 (短い方)period_long
: 移動平均を取る期間 (長い方)instrument
: 対象とする通貨ペアamount_jpy
: ゴールデンクロス・デッドクロス時にBUY/SELLする金額 (日本円)
period_base = 'D' period_short = 5 period_long = 20 instrument = 'USD_JPY' amount_jpy = 100000
OandaV20で指定可能な単位期間については、こちらのリンクにもありますが、下記の通りです。

ローソク足の取得
移動平均線を作るために、過去のローソク足を取得します。DataFrame型の表 (df
) を用意して、そこに値を入れます。df.loc[i] = 配列
とすると、DataFrame型の表 (i行目
) に行を追加していくことができます (df
と配列で、列数が同じである必要がある点には注意が必要です)。
時間 (Time) にはPCの時間が入るので、日本時間とはずれている可能性があります。少し注意をしてください (EC2/Cloud9の場合は、date
コマンドで現在時刻を確認できます)。
import pandas as pd df = pd.DataFrame(columns=['Time', 'Open', 'High', 'Low', 'Close', 'Volume']) import oandapyV20.endpoints.instruments as instruments params = { "count": period_long+1, "granularity": period_base } r = instruments.InstrumentsCandles(instrument=instrument, params=params) api.request(r) res = r.response for i, c in enumerate(res['candles']): df.loc[i] = [['time'].split('.')[0], c['mid']['o'], c['mid']['h'], c['mid']['l'], c['mid']['c'], c['volume']]
ここで念の為df
を表示させると、中身は下記の通りです。
# print(df) Time Open High Low Close Volume 0 2020-10-29T21:00:00 104.614 104.744 104.126 104.620 71785 1 2020-11-01T22:00:00 104.538 104.950 104.534 104.695 52044 2 2020-11-02T22:00:00 104.711 104.802 104.433 104.531 77477 3 2020-11-03T22:00:00 104.522 105.346 104.150 104.512 167942 4 2020-11-04T22:00:00 104.516 104.548 103.442 103.504 111695 5 2020-11-05T22:00:00 103.498 103.760 103.177 103.370 111245 6 2020-11-08T22:00:00 103.331 105.648 103.190 105.368 138596 7 2020-11-09T22:00:00 105.378 105.488 104.822 105.302 106965 8 2020-11-10T22:00:00 105.307 105.678 105.006 105.432 66100 9 2020-11-11T22:00:00 105.437 105.479 105.070 105.134 67868 10 2020-11-12T22:00:00 105.127 105.156 104.565 104.640 64487 11 2020-11-15T22:00:00 104.730 105.138 104.364 104.583 75587 12 2020-11-16T22:00:00 104.587 104.614 104.071 104.197 63753 13 2020-11-17T22:00:00 104.192 104.208 103.652 103.824 67176 14 2020-11-18T22:00:00 103.820 104.218 103.718 103.749 69388 15 2020-11-19T22:00:00 103.803 103.912 103.704 103.859 52049 16 2020-11-22T22:00:00 103.798 104.638 103.683 104.517 66521 17 2020-11-23T22:00:00 104.526 104.763 104.146 104.447 72321 18 2020-11-24T22:00:00 104.444 104.600 104.256 104.458 65141 19 2020-11-25T22:00:00 104.463 104.478 104.216 104.275 47197 20 2020-11-26T22:00:00 104.266 104.284 103.907 104.108 46331
移動平均の取得
DataFrame型
(正しくはSeries型
) のrolling関数
を利用して移動平均を求めます。df['Close']
で、終値の移動平均を利用することとしています。簡単で良いですね。
df_lperiod = df['Close'].rolling(period_long).mean() df_speriod = df['Close'].rolling(period_short).mean()
こちらも一応出力させると、下記の通り。例では20 or 5日の平均を取るにも関わらず21個しかデータを渡していないので、出力結果の前半はNaN
になっています。これはこれで、正しい出力結果です。
# print(df_lperiod) 0 NaN 1 NaN 2 NaN 3 NaN 4 NaN 5 NaN 6 NaN 7 NaN 8 NaN 9 NaN 10 NaN 11 NaN 12 NaN 13 NaN 14 NaN 15 NaN 16 NaN 17 NaN 18 NaN 19 104.45085 20 104.42525 Name: Close, dtype: float64 # print(df_speriod) 0 NaN 1 NaN 2 NaN 3 NaN 4 104.3724 5 104.1224 6 104.2570 7 104.4112 8 104.5952 9 104.9212 10 105.1752 11 105.0182 12 104.7972 13 104.4756 14 104.1986 15 104.0424 16 104.0292 17 104.0792 18 104.2060 19 104.3112 20 104.3610 Name: Close, dtype: float64
ゴールデンクロス・デッドクロスの判定
ゴールデンクロスの判定は、「昨日までは5日平均の方が20日平均より小さかったのに、今日で5日平均の方が大きくなった」です。デッドクロスの判定は、これの逆です。
golden_cross
、dead_cross
関数についてはこの後すぐ、記載しています。結果が気になるので、line_notify
関数でLINE通知をさせるようにしています。このline_notify
については、過去記事を参照ください。
if df_speriod[-2] < df_lperiod[-2] and df_speriod[-1] > df_lperiod[-1]: golden_cross() line_notify('OANDA BOT: GOLDEN CROSS') elif df_speriod[-2] > df_lperiod[-2] and df_speriod[-1] < df_lperiod[-1]: dead_cross() line_notify('OANDA BOT: DEAD CROSS') else: line_notify('OANDA BOT: NO ACTION)
BUY/SELL処理
ゴールデンクロスが出たときにはBUY処理、デッドクロスが出たときにはSELL処理をします。なお、BUY処理・SELL処理をする際、今まで持っていたポジションは全て解消する (all_position_close関数
) ものとします。
下記の関数を、前述の「ゴールデンクロス・デッドクロスの判定」の前に入れ込みます (pythonの処理の関係です)。
def golden_cross (): global amount_jpy all_position_close() market_order(amount_jpy) def dead_cross (): global amount_jpy all_position_close() market_order(amount_jpy)
上記で呼び出している関数の中身は、下記のものです。
all_position_close
: 持っているポジションをOpenPositions
で取得して、その中身をfor文
で確認しながら、持っているポジションをPositionClose
で閉じていきます。
import oandapyV20.endpoints.positions as positions def all_position_close (): global accountID r = positions.OpenPositions(accountID=accountID) api.request(r) for p in r.response['positions']: inst = p['instrument'] long_ = 0 if p['long']['units'] == 0 else p['long']['units'] short_ = 0 if p['short']['units'] == 0 else p['short']['units'] data = {} if long_ != 0 or short_ != 0: if long_ != 0: data = {'longUnits': 'ALL'} elif short_ != 0: data = {'shortUnits': 'ALL'} r = positions.PositionClose(accountID=accountID, instrument=inst, data=data) api.request(r)
market_order
: 日本円で額を指定して、その分売ったり買ったりします。額がプラスだと買って、マイナスだと売ります。関数中のjpy_convert
については後述。
LIMIT/MARKETの議論は本記事の本質ではないので、MARKET (=成行)売買のみの実装です。
import oandapyV20.endpoints.orders as orders def market_order (amount_jpy): global instrument, accountID unit = jpy_convert(amount_jpy, instrument) data = {'order': {'instrument': instrument, 'units': str(unit), 'type': 'MARKET' }} r = orders.OrderCreate(accountID, data=data) api.request(r)
jpy_convert
: 引数で指定した日本円分だけinstrument
を売り買いする場合、何unit
が適正か計算して返す関数です。数百円分はズレることがあるので、ご利用の際はご注意ください。現在、XXX_JPY
の通過ペアにのみ対応しています。
import oandapyV20.endpoints.pricing as pricing def jpy_convert(amount_jpy, instrument): global accountID if instrument[-3:] != 'JPY': print('"jpy_convert" can convert only instrument of "XXX_JPY", cannot covert', instrument) exit() params = {'instruments': instrument} r = pricing.PricingInfo(accountID=accountID, params=params) rv = api.request(r) ab = (float(rv['prices'][0]['closeoutBid']) +float(rv['prices'][0]['closeoutAsk'])) / 2 provisional_unit = int(abs(amount_jpy) / ab) sign = 1 if amount_jpy > 0 else -1 return sign * provisional_unit
プログラムの自動実行
上述のプログラムを手動で毎回起動するのは手間なので、自動化させます。
EC2/Linuxを利用されている方であれば、こちらの記事を参考にしつつ、crontab
コマンドを入れてファイルを編集するだけです。下記の例では”0 10 * * *
“という指定で、毎日10時に起動するようにしています。
間違えても修正可能なので、あまりビビらなくくても良い部分です。
# 下記のコマンドを入れ、 $ crontab -e # 下記の行を最下部に追加する 0 10 * * * /usr/bin/python3 /home/ec2-user/environment/oanda1.py
なお、対象ファイルのパスがどこかは、対象ファイルのディレクトリでpwd
コマンドを打つことで確認できます。
$ pwd # /home/ec2-user/environment
pythonのパスがどこに通っているかは、which
コマンドで確認できます。
$ which python3 # /usr/bin/python3
crontab
を設定してもEC2/Linuxが自動で起動するわけではないので、該当の時間にはEC2/Linuxが自動起動するような仕組みを準備する必要があります。
EC2/Cloud9を利用しているのであれば、AWS LambdaとAWS CloudWatchを組み合わせることで自動起動させることができます。こちらの手順は少しややこしいので、別記事で扱います。
(Pythonのscheduleというライブラリを使って自動実行させる方法もあるのですが、こちらはEC2/Linuxを起動しっぱなしにする必要があり、お財布に優しくないため割愛します)
まとめ
OandapyV20を使って、ゴールデンクロスで買い、デッドクロスで売るBOT/シストレの構築方法を紹介しました。
ネット上にも、jpy_convert
とcrontab
について触れている記事は多くないので、参考にしていただければと思います。