シストレどうですか

  Algorithmic Trading for Dummies

FXボット とりあえず作ってみた編 その2 ー レートの取得

前回 からの続きでプロトタイプボットの概要設計の中から、全体の構造と最新のレートをOANDAサーバーから取得する部分から考えていきたいと思います。


全体の構造


前回書いたフローチャートをもとに全体の構造を作ってみます。

f:id:jantzen:20211010032734p:plain:w250
フローチャート

まずは学問に王道なしという事で、地道にフローチャートをもとに枠だけ作ってみます。

#外部モジュールの定義など 
  
#1. 初期処理  
def Init():
  pass
#2. 最新レートの取得
def NewRate():
  pass
#3. ポジションの確認
def Position():
  pass
#4. シグナル判定
def Signal():  
  pass
#5. 注文
def Order():
  pass
#6. 待機
def Wait():  
  pass
  
if __name__ == "__main__":
  
  Init()
  
  for i in range(1):
    
    NewRate() 
    Position()   
    Signal()     
    Order()
    Wait()  
    
  #ループ戻り

とりあえずフローチャートをもとに枠だけ作ってみるとこんな感じになってしまいます。 というわけですので早速中身も加えてみます。


処理の記述


まず簡単な1. 初期処理、2. 最新レートの取得と最後の6. 待機処理について考えてみます。(下図の赤い部分)

f:id:jantzen:20211010032738p:plain:w250
フローチャート

最新レートの取得と待機

1. 初期処理

頭の部分は外部モジュールや定数の定義ですので、それに加えてサーバーとの接続の部分までを初期処理と考えてみます。 定数の定義としては、Oandaの口座情報や取引銘柄、それに加え取得したデータから計算するのに必要なパラメータなどを定義しておきます。

2. 最新レートの取得

onadapyV20のendpoints.pricingを使って取得を行います。
取得した情報からまずは現在取引可能な状態かどうかを確認します。
今の段階ではまずはサーバーから最新のレートが取得できたかどうか確認するために取得した情報をprint関数で表示させます。

エンドポイントの引数については以下の記事も参考にしてください。
jantzen.hatenablog.com jantzen.hatenablog.com

6. 待機処理

timeモジュールのSleep関数を使って指定した秒数分待機させます。とりあえず5秒としておきます。 また、ループの回数もとりあえず5回のみ回るように指定しておきます。

サンプル

ここまでの処理を追加したものをコーディングしてみました。
初期処理と待機処理は少ないのでdefにまとめず直接記述しています。

#外部モジュールの読み込み
from oandapyV20 import API
import oandapyV20.endpoints.pricing as pricing
import time
import json
 
#定数
#口座情報(自分の情報を入力)
TOKEN = ""
ID = ""
  
    
def CurrentRate():  #2. 最新レートの取得
  
  params = {
          "instruments": "USD_JPY"
        }
  
  r = pricing.PricingInfo(accountID=ID, params=params)
  rv = api.request(r)
  print(json.dumps(rv, indent=2))
  
    
def Position():  #3. ポジションの確認
  pass
  
def Signal():  #4. シグナル判定
  pass
  
def Order():  #5. 注文
  pass

    
if __name__ == "__main__":
  
  api = API(access_token= TOKEN)
  
  for i in range(4):
    
    CurrentRate() 
    Position()   
    Signal()     
    Order()
 
    time.sleep(5)
    
  #ループ戻り

デバッグモードで走らせてみると、以下のようなデータが指定した時間毎に表示されます。

{
  "time": "2021-08-31T05:49:25.126065950Z",    
  "prices": [
    {
      "type": "PRICE",
      "time": "2021-08-31T05:48:53.630316365Z",
      "bids": [
        {
          "price": "109.801",
          "liquidity": 10000000
        }
      ],
      "asks": [
          "price": "109.814",
          "liquidity": 10000000
        }
      ],
      "closeoutBid": "109.801",
      "closeoutAsk": "109.814",
      "status": "tradeable",
      "tradeable": true,
      "quoteHomeConversionFactors": {
        "positiveUnits": "0.00910631",
        "negativeUnits": "0.00910739"
      },
      "instrument": "USD_JPY"
    }
  ]
}

計算処理の追加

上の例では最新のレートを表示するだけでしたが、次の処理につなげるために受け取ったデータから必要な値を取っていきたいと思います。

取得したデータから必要な情報を取り出す。

取得したデータから取引を行うのに必要な情報を取得します。
具体的には以下の情報を取得します。

取得情報 場所 説明
トレード可能フラグ ['prices'][0]['tradeable'] トレード可能の状態かどうか判断する。 True=可能、False=不可能
bid ['prices'][0]['closeoutBid'] 売値 プライス
ask ['prices'][0]['closeoutAsk'] 買値 プライス
時間 ['time'] 更新時間 DateTimeフォーマット

必要な情報を計算する。

次に取得したビッド(bid)値とアスク(ask)値からスプレッド(spread)を計算します。 トレードが可能な状態であってもスプレッドが通常より広がっている場合があり、このような状態の時に注文しても利益がでる可能性が極めて少ないため一定以上の値幅がある場合は注文しないようにします。
取り出したプライスはstring値なので直接計算出来ませんので数値に変換して計算しますが、float値に変換した場合、誤差がでて小数部が3桁以上になる場合があるので気をつけて下さい。 精度はともかくも、あとあとそれらの値を使ってセットした引数によってはエンドポイントを呼び出す時に桁あふれでエラーになる場合がありますので、有効桁数で四捨五入するかDecimal関数を使って10進数で計算するようにします。

計算 場所 説明
spread ask - bid 買値と売値の差

次の処理へ進めるか判定する。

取得および計算した値から次の処理へ進むかどうかの判定を行います。 以下の条件でボットを終了させるか継続するか決定します。

条件 アクション 内容
トレード可能フラグ=False ボットを停止 トレードできない時はボットを停止する。
spread>最大許容スプレッド 待機処理へスキップ スプレッドが拡大中の時は待機処理までスキップする。

結果を停止の場合は”STOP”、スキップする場合は”SKIP”、その他の場合は”GO”の値をstatusとして戻します。

"トレード可能フラグ"ですが、週末はお休みですので、その間は値が"False"になりますが、NY時間の午後5時頃にメインテナンスの為か短時間の間トレード可能フラグはFalseになったりもするようです。そのような場合はボットを停止しないようにリトライさせる手もありますが、初期型のボットは処理をシンプルにするためにすべて停止という事にしておきます。

"最大許容スプレッド"に関しても同様に思わぬところで拡がったりすることがあります。
USD/JPYの場合は通常1.5pip前後で取引されているので、今回は2pip (0.02円)としておきます。

例外処理他の追加

最後に例外処理やデバッグしやすいようにprint関数も追加します。

例外処理

サーバーからのエラーの応答やプログラム内の問題によるエラーが拾えるようにtry-exceptを追加します。とりあえずエラーの内容がわかればよいのでメインの部分に加えておきます。
ボットが停止した時間がわかるように停止した時点の時刻を取得できるようにdatetimeモジュールも追加します。

その他処理の追加

動かした時に何がおきているかわかるように、取得した情報の内容やボットのステータスがわかるような部分にprint関数を追加しておきます。
通貨銘柄や通貨の桁数などの情報も定数で指定しておけば、通貨銘柄を変更したい時でも修正箇所が減りますので間違いが起こりにくくなります。加えてループの回数や待機時間も定数にしておきます。


サンプルプログラム


以上のすべての処理を追加したものをコーディングしてみました。

#外部モジュール
from oandapyV20 import API
import oandapyV20.endpoints.pricing as pricing
  
import time
import datetime
   
    
#口座情報(自分の情報を入力)
TOKEN = ""
ID = ""
    
#取引通貨  
INSTRUMENT = "USD_JPY"
#レート桁数
DECIMALS = 3 
#Pip桁数
PIP_LOCATION = -2 
 
#最大許容スプレッド  
MAX_SPREAD_PIPS = 2 #Pips
     
#ループ回数
LOOP = 100 #回
#待機時間
WAIT = 5 #秒
    
    
def CurrentRate():
      
  #最新レートの取得
  params = {
          "instruments": INSTRUMENT
        }
         
  r = pricing.PricingInfo(accountID=ID, params=params)
  rv = api.request(r)
          
  #スプレッドの計算
  bid = rv['prices'][0]['closeoutBid']
  ask = rv['prices'][0]['closeoutAsk']
  spread = round(float(ask) - float(bid), DECIMALS)
          
  #トレード可能?
  if rv['prices'][0]['tradeable'] == True:
    max_spread = MAX_SPREAD_PIPS * (10 ** PIP_LOCATION)
    if spread < max_spread:
      status = "GO"
    else: #スプレッド拡大中
      status = "SKIP"
  #クローズ/メンテ中
  else:
    status = "STOP"
            
  #戻り値  
  return {'status': status, 'bid': bid, 'ask': ask, 'spread': spread}
      
  
def Position():  
  pass  
      
def Signal():
  pass
      
def Order():
  pass 
  
  
if __name__ == "__main__":
    
  try:
    
    api = API(access_token= TOKEN)
      
    for i in range(LOOP):
    
      #最新レート確認  
      r_rate = CurrentRate()  
      print(r_rate)
      #次の処理
      if r_rate['status'] == "GO":
        print("継続-トレード可能")
  
        Position()             
        Signal()
        Order()
    
      #スプレッドが広すぎる
      elif r_rate['status'] == "SKIP":
        print("スキップ-スプレッド拡大中")    
    
      #マーケットがクローズ(またはメンテ中)
      elif r_rate['status'] == "STOP":
        print("停止ーマーケットクローズ")    
        break #ボット終了
          
      else:
        print("停止ー予期せぬエラー発生")    
        break #ボット終了
    
      #次のサイクル
      print("待機中 ", i) 
      time.sleep(WAIT)
      
  except Exception as e:
    print(e) 
    
  finally:
    print("Botが停止しました。 UTC:", datetime.datetime.now(datetime.timezone.utc))

デバッグモードで動かしてみると、定期的に以下のようなメッセージが表示されると思います。

{'status': 'GO', 'bid': '109.991', 'ask': '110.005', 'spread': 0.014}
継続-トレード可能
待機中  0
{'status': 'GO', 'bid': '109.991', 'ask': '110.005', 'spread': 0.014}
継続-トレード可能
待機中  1
{'status': 'GO', 'bid': '109.991', 'ask': '110.005', 'spread': 0.014}
継続-トレード可能
待機中  2
{'status': 'GO', 'bid': '109.991', 'ask': '110.005', 'spread': 0.014}
継続-トレード可能
待機中  3
{'status': 'GO', 'bid': '109.993', 'ask': '110.007', 'spread': 0.014}
継続-トレード可能
待機中  4


まとめ


最新のレートを取得し、そこで得られた情報から次の処理へ進むか、ボットを停止させるかの判定を行うという部分が完成しました。
まだ取引を行う訳ではありませんのでしばらく動かしてみて、どんな時にボットが停止したりスキップしたりするのか確認しながら様子をみていきたいとも思います。

次回はこれにポジションを保有しているかどうか確認する処理を追加していきたいと思います。