シストレ

OANDA シストレ(oandapyV20) の始め方 【BOT編】

投稿日:2020年11月29日 更新日:

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_crossdead_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_convertcrontabについて触れている記事は多くないので、参考にしていただければと思います。



-シストレ
-, , , , , ,

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です



関連記事

no image

OANDA シストレ(oandapyV20) の始め方 【関数編3】

OANDAで、シストレ/APIを利用する際にお世話になる関数の紹介です。 OANDA シストレ(oandapyV20) の始め方 記事一覧 導入編関数編1関数編2関数編3 ←本記事Pandas編(作成 …

no image

OANDA シストレ(oandapyV20) の始め方 【関数編2】

OANDAで、シストレ/APIを利用する際にお世話になる関数の紹介です。情報は、基本的に公式ドキュメント (英語) から引っ張ってきています。 OANDA シストレ(oandapyV20) の始め方 …

no image

OANDA シストレ(oandapyV20) の始め方 【Pandas編】

OANDAで、シストレ/APIを利用する際にお世話になるPandasの紹介です。 OANDA シストレ(oandapyV20) の始め方 記事一覧 導入編関数編1関数編2関数編3Pandas編 ←本記 …

no image

OANDA シストレ(oandapyV20) の始め方 【関数編1】

OANDAで、シストレ/APIを利用する際にお世話になる関数の紹介です。情報は、基本的に公式ドキュメント (英語) から引っ張ってきています。 OANDA シストレ(oandapyV20) の始め方 …

no image

OANDA シストレ(oandapyV20) の始め方 【導入編】

OANDAを Cloud9 (AWS) + OandapyV20 (Python) 環境で始めるための、クイックスタート記事です。 OANDA シストレ(oandapyV20) の始め方 記事一覧 導 …

プロフィール

タクマ
−−−−−−−−

東南アジア(ミャンマー&フィリピン)でNW系システムインテグレーターとして6年ほど駐在していました。本を1000冊以上読んだり、プログラミングをしていたりします。嫁さんはタイ人です。
ーー
Twitterまとめのまとめ
YouTube Find!
SNS Trends

−−−−−−−−