シストレどうですか

  Algorithmic Trading for Dummies

FXボット とりあえず作ってみた編 その5 ー 発注処理

いよいよとりあえず作ってみた編の最後になりますが、発注処理の部分について考えてみたいと思います。


注文処理


売買シグナル判定処理で売買シグナルが発生した場合は、必要な引数をセットして注文用のエンドポイントを呼び出します。
今回の範囲は下のフローチャートの赤い部分になります。

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

処理の概要

処理内容をなるべくシンプルにというコンセプトですので資金管理の処理は省きます。 よって成行注文のみを行い売買のサイズは固定値とします。また手仕舞いに関しては成行注文の際に同時に設定した利確・損切注文で行います。

利確・損切注文の仕様

利確・損切注文の価格については約定価格からの値幅を指定するようにします。
これは、成行注文の場合は約定価格が約定するまでわかりませんので、利確・損切を価格でセットしてしまうとタイミングによっては成行注文がエラーになったり、いきなり利確損切注文が執行されてしまったりする場合があるためです。

というわけですので、指定する値幅に関しては完成した最後の終値と過去の指定した本数までさかのぼってその中の最安値(または最高値)との差額に一定の値を加算したものを値幅としてみます。 また、利確注文も損切注文も同じ値幅にします。

f:id:jantzen:20211001143207p:plain:w300
利確・損切の値幅ぎめ(左:赤三兵、右:黒三兵)

処理の追加

以上の仕様をもとに注文処理の部分を追加していきます。

エンドポイントの追加

注文する為に必要なエンドポイントはonadapyV20のendpoints.orderになります。

import oandapyV20.endpoints.orders as orders

関数の定義

いままでの処理で得た値を受け取り、その値を元に計算を行って注文するのに必要な引数をセットできるようにします。
「1. 最新レートの取得」処理の戻り値と「4. シグナル判定」処理の戻り値とそこで使ったろうそく足を保持しているデータフレームを引数としてその内容を受け取ります。(データフレームは主に利確・損切用の値幅の計算に使います。)

def Order(r_rate,r_signal): 

利確・損切用の値幅の計算

注文する前に利確・損切注文用の値幅の計算が必要ですので、前述の利確・損切注文の仕様どおり最後のろうそく足の終値とそれを含む3本のろうそく足の中で最安値(または最高値)との差額に一定の値を加えたものを値幅とします。
また計算結果の値幅が現在のスプレッドより狭すぎる場合はエラーになってしまいますので、計算後確認します。

定数の定義

#注文用
UNITS = 1
N = 2 #Pips

計算部分

  #最後のローソク足の終値の取得
  xclose = r_signal['df']['close'].iat[-1]
      
  #区間内の最高値と最安値の取得
  xmin = r_signal['df']['low'].min()
  xmax = r_signal['df']['high'].max()
  
  print("終値: %s 最高値: %s 最安値: %s" %(xclose, xmax, xmin))
      
  #Risk(Spread)の計算
  if r_signal['signal'] == 1:
    #買:区間の最安値と直前の足の終値の差にN Pips足す
    distance = round(xclose - xmin, DECIMALS)
  elif r_signal['signal'] == -1:
    #売:区間の最高値と直前の足の終値の差にN Pips足す
    distance = round(xmax - xclose, DECIMALS)
  
  #計算した値幅に一定の値を加える  
  distance2 = distance + N * (10 ** PIP_LOCATION)
    
  print("値幅: %s 値幅+N: %s スプレッド: %s" %(distance, distance2, r_rate['spread']))
  
  #計算結果より現在のスプレッドと比較
  if distance2 < r_rate['spread']:
    print("値幅が足りません。")
    return {'status': 'SKIP'}

注文

注文に必要な情報がすべてそろった状態になったらそれらの値を引数として、エンドポイントを呼び出します。
成行注文に損切・利確注文も同時に行いますので必要な引数は以下のようになります。

引数名 説明 このボットでの使い方
type トレードタイプ "MARKET" (成行) 固定値
instrument 銘柄名 ”USD_JPY" 定数からセット(取引したい通貨組を指定)
units 注文単位 任意の整数 買い = 定数 * 1、売り = 定数 * -1
takeProfitOnFill distance 値幅 計算値 利確・損切用の値幅の計算の結果をセット*
stopLossOnFill distance 値幅 計算値 利確・損切用の値幅の計算の結果をセット
*マニュアル上では"takeProfitOnFill"引数の"distance"は使えない事になっています。

定数

INSTRUMENT = "USD_JPY"
UNITS = 1

引数のセット

  #発注データのセット      
  data = {
        "order": {
            "type": "MARKET",
            "instrument": INSTRUMENT,
            "units": str(UNITS * r_signal['signal']),
            "takeProfitOnFill": {
                "distance": str(distance2)
            },
            "stopLossOnFill": {
                "distance": str(distance2)
            },
        }
    }
  
  #注文用エンドポイントの呼び出し
  r = orders.OrderCreate(accountID=ID, data=data)
  rv = api.request(r)

注文結果の確認

最後にサーバーに送信した注文データがどうなったか結果の確認を行います。成行注文の場合は約定したかどうかすぐに結果がわかりますので、エンドポイントからの戻り値を調べます。
注文の部分ですので暴走すると大変な事になりますし初心者用のボットなので予期せぬエラーも発生する事を想定して、なにか異常があった場合はボットを停止させます。

  #結果確認
  print(json.dumps(rv, indent=2))
  
  if r.status_code == 201:
    if "orderFillTransaction" in rv.keys():
      status = "ORDERED"
      result = rv['orderFillTransaction']['orderID']
    elif 'orderCancelTransaction' in rv.keys():  
      status = "STOP"
      result = rv['orderCancelTransaction']['reason']  
    else:
      status = "STOP"  
      result = "予期せぬエラー(Status = 201)"
  else:
    status = "STOP"
    result = "予期せぬエラー(Status != 201)"
    
  print(status, " : " ,result)  
    
  #戻り値のセット
  return {'status': status}

注文の部分ですから少し細かくエラーを拾えるようにしておきます。
無事約定された場合はOrder IDを、エラーが起きた場合はそのエラーメッセージを取得して表示させます。 サーバーから正常終了(201)が返ってきても、損切注文の値幅が狭すぎたり、注文の条件が合わない場合キャンセルで戻ってきます。 ステータスコードが201以外の場合も想定してますが、ラッパーのほうでエラーを拾ってくれると思いますので必要ないと思いますが、まだよくわかっていないのでとりあえず入れてあります。


プログラムサンプル


いままでの分に発注処理を追加した全体のプログラム例となります。
ろうそく足が5分間隔なので待機時間も60秒くらいにして、1週間くらい続けて動くようにループ回数も1万回に増やします。

プログラム

#外部モジュール
from oandapyV20 import API
import oandapyV20.endpoints.pricing as pricing
import oandapyV20.endpoints.positions as positions   
import oandapyV20.endpoints.instruments as instruments 
import oandapyV20.endpoints.orders as orders   #👈注文用エンドポイントの追加 
  
import json
import time
import datetime
import pandas as pd
        
#口座情報(自分の情報を入力)
TOKEN = ""
ID = ""
        
#取引通貨  
INSTRUMENT = "USD_JPY"
#レート桁数
DECIMALS = 3 
#Pip桁数
PIP_LOCATION = -2 
    
#最大許容スプレッド  
MAX_SPREAD_PIPS = 2 #Pips
        
#ループ回数
LOOP = 10000 #回
#待機時間
WAIT = 60 #秒
      
#ろうそく足取得用
COUNT = 10 #ろうそく足の取得本数
GRANULARITY = "M5" #時間足(5分)
      
#👉注文用
UNITS = 1
N = 2 #Pips
    
    
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():  
        
  #ポジションの確認処理追加
  r = positions.PositionDetails(accountID=ID, instrument=INSTRUMENT)
  rv = api.request(r)
        
  if rv['position']['long']['units'] != "0" or rv['position']['short']['units'] != "0": 
    print("ポジあり。待機")
    status = "SKIP"         
  else:
    print("ポジなし。 継続")
    status =  "GO"
      
  return {'status': status}
      
      
def Signal():  #シグナル判定(赤3黒3)
        
  #ろうそく足の取得
  #引数セット
  params = {
          "count": COUNT,
          "granularity": GRANULARITY
        }
  #エンドポイント呼び出し
  r = instruments.InstrumentsCandles(instrument=INSTRUMENT, params=params)
  rv = api.request(r)
        
  #データフレームへの変換
  df = pd.json_normalize(rv, record_path='candles', meta=['instrument', 'granularity'], sep='_')
  #コラム名の変更
  df.columns = ['complete', 'volume', 'time_UTC', 'open', 'high', 'low', 'close', 'pair', 'ashi']
          
  #完成形のろうそく足を最後から3本分のみ取得
  df = df[df['complete'] == True].tail(3)
      
  #計算用に属性を変更
  df = df.astype({'open': float, 'close': float, 'high': float, 'low': float})
        
  #3本分のろうそく足毎のトレンドの判定
  df.loc[round(df['close'] - df['open'],DECIMALS) > 0, 'trend'] = 1  #陽線(赤)
  df.loc[round(df['close'] - df['open'],DECIMALS) < 0, 'trend'] = -1 #陰線(黒)
  df.loc[round(df['close'] - df['open'],DECIMALS) == 0, 'trend'] = 0  #同じ
        
  #売買シグナルの判定
  if df.trend.sum() == 3: #すべて陽線(上昇)
    signal = 1
    print("買シグナル!")
  elif df.trend.sum() == -3: #すべて陰線(下降)
    signal = -1
    print("売シグナル!")
  else:
    signal = 0
    print("シグナルなし")
      
  print(df)
        
  #戻り値のセット
  return {'signal': signal, 'df': df}
    
      
#👇#    
def Order(r_rate, r_signal): 
    
  #発注処理
  #最後のローソク足の終値の取得
  xclose = r_signal['df']['close'].iat[-1]
      
  #区間内の最高値と最安値の取得
  xmin = r_signal['df']['low'].min()
  xmax = r_signal['df']['high'].max()
  
  print("終値: %s 最高値: %s 最安値: %s" %(xclose, xmax, xmin))
      
  #Risk(Spread)の計算
  if r_signal['signal'] == 1:
    #買:区間の最安値と直前の足の終値の差にN Pips足す
    distance = round(xclose - xmin, DECIMALS)
  elif r_signal['signal'] == -1:
    #売:区間の最高値と直前の足の終値の差にN Pips足す
    distance = round(xmax - xclose, DECIMALS)
    
  #計算した値幅に一定の値を加える  
  distance2 = round(distance + N * (10 ** PIP_LOCATION), DECIMALS)
      
  print("値幅: %s 値幅+N: %s スプレッド: %s" %(distance, distance2, r_rate['spread']))
    
  #計算結果より現在のスプレッドと比較
  if distance2 < r_rate['spread']:
    print("値幅が足りません。")
    return {'status': 'SKIP'}
    
      
  #発注データのセット      
  data = {
        "order": {
            "type": "MARKET",
            "instrument": INSTRUMENT,
            "units": str(UNITS * r_signal['signal']),
            "takeProfitOnFill": {
                "distance": str(distance2)
            },
            "stopLossOnFill": {
                "distance": str(distance2)
            },
        }
    }
      
  #発注
  r = orders.OrderCreate(accountID=ID, data=data)
  rv = api.request(r)

  #結果確認
  print(json.dumps(rv, indent=2))

  if r.status_code == 201:
    if "orderFillTransaction" in rv.keys():
      status = "ORDERED"
      result = rv['orderFillTransaction']['orderID']
    elif 'orderCancelTransaction' in rv.keys():  
      status = "STOP"
      result = rv['orderCancelTransaction']['reason']  
    else:
      status = "STOP"  
      result = "予期せぬエラー(Status = 201)"
  else:
    status = "STOP"
    result = "予期せぬエラー(Status != 201)"
  
  print(status, " : " ,result)  
  
  #戻り値のセット
  return {'status': status}
#👆#

              
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("継続-トレード可能")
        
       #保有ポジションの確認
        r_pos = Position()             
        #ポジション無しの時
        if r_pos['status'] == "GO":   
          #売買シグナルの判定
          r_signal = Signal()
          if r_signal['signal'] != 0: #売買シグナルがでたら注文処理へ
            #👇#    
            #注文処理
            r_order = Order(r_rate, r_signal)
            if r_order['status'] != "ORDERED" and r_order['status'] != "SKIP":
              print("停止ー発注エラー")    
              break #ボット終了
            #👆#
              
      #スプレッドが広すぎる
      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))

確認が必要なポイントにはprint関数を入れておきます。多めに入れてありますが必要がなくなり次第削っておきます。

ターミナル

{'status': 'GO', 'bid': '113.578', 'ask': '113.593', 'spread': 0.015}
継続-トレード可能
ポジなし。 継続
シグナルなし
   complete  volume                        time_UTC  ...     pair  ashi  trend
6      True      37  2021-10-28T23:30:00.000000000Z  ...  USD_JPY    M5   -1.0
7      True      35  2021-10-28T23:35:00.000000000Z  ...  USD_JPY    M5    1.0
8      True      27  2021-10-28T23:40:00.000000000Z  ...  USD_JPY    M5    1.0

[3 rows x 10 columns]
待機中  54
{'status': 'GO', 'bid': '113.578', 'ask': '113.593', 'spread': 0.015}
継続-トレード可能
ポジなし。 継続
買シグナル!
   complete  volume                        time_UTC  ...     pair  ashi  trend
6      True      35  2021-10-28T23:35:00.000000000Z  ...  USD_JPY    M5    1.0
7      True      27  2021-10-28T23:40:00.000000000Z  ...  USD_JPY    M5    1.0
8      True      29  2021-10-28T23:45:00.000000000Z  ...  USD_JPY    M5    1.0

[3 rows x 10 columns]
終値: 113.584 最高値: 113.587 最安値: 113.561
値幅: 0.023 値幅+N: 0.043 スプレッド: 0.015
{
  "orderCreateTransaction": {
    "id": "1647",
    "accountID": "999-999-99999999-001",
    "userID": 12083731,
    "batchID": "1647",
    "requestID": "24874035994337514",
    "time": "2021-10-28T23:50:22.878995851Z",
    "type": "MARKET_ORDER",
    "instrument": "USD_JPY",
    "units": "1",
    "timeInForce": "FOK",
    "positionFill": "DEFAULT",
    "takeProfitOnFill": {
      "distance": "0.043",
      "timeInForce": "GTC"
    },
    "stopLossOnFill": {
      "distance": "0.043",
      "timeInForce": "GTC",
      "triggerMode": "TOP_OF_BOOK"
    },
    "reason": "CLIENT_ORDER"
  },
  "orderFillTransaction": {
    "id": "1648",
    "accountID": "999-999-99999999-001",
    "userID": 12083731,
    "batchID": "1647",
    "requestID": "24874035994337514",
    "time": "2021-10-28T23:50:22.878995851Z",
    "type": "ORDER_FILL",
    "orderID": "1647",
    "instrument": "USD_JPY",
    "units": "1",
    "requestedUnits": "1",
    "price": "113.593",
    "pl": "0.0000",
    "quotePL": "0",
    "financing": "0.0000",
    "baseFinancing": "0",
    "commission": "0.0000",
    "accountBalance": "98891.6526",
    "gainQuoteHomeConversionFactor": "0.008759920979",
    "lossQuoteHomeConversionFactor": "0.008847960386",
    "guaranteedExecutionFee": "0.0000",
    "quoteGuaranteedExecutionFee": "0",
    "halfSpreadCost": "0.0001",
    "fullVWAP": "113.593",
    "reason": "MARKET_ORDER",
    "tradeOpened": {
      "price": "113.593",
      "tradeID": "1648",
      "units": "1",
      "guaranteedExecutionFee": "0.0000",
      "quoteGuaranteedExecutionFee": "0",
      "halfSpreadCost": "0.0001",
      "initialMarginRequired": "0.0500"
    },
    "fullPrice": {
      "closeoutBid": "113.578",
      "closeoutAsk": "113.593",
      "timestamp": "2021-10-28T23:50:21.380182675Z",
      "bids": [
        {
          "price": "113.578",
          "liquidity": "10000000"
        }
      ],
      "asks": [
        {
          "price": "113.593",
          "liquidity": "10000000"
        }
      ]
    },
    "homeConversionFactors": {
      "gainQuoteHome": {
        "factor": "0.00875992097881"
      },
      "lossQuoteHome": {
        "factor": "0.00884796038563"
      },
      "gainBaseHome": {
        "factor": "1"
      },
      "lossBaseHome": {
        "factor": "1"
      }
    }
  },
  "relatedTransactionIDs": [
    "1647",
    "1648",
    "1649",
    "1650"
  ],
  "lastTransactionID": "1650"
}
ORDERED  :  1647
待機中  55
{'status': 'GO', 'bid': '113.581', 'ask': '113.597', 'spread': 0.016}
継続-トレード可能
ポジあり。待機
待機中  56
{'status': 'GO', 'bid': '113.579', 'ask': '113.593', 'spread': 0.014}
継続-トレード可能
ポジあり。待機
待機中  57
{'status': 'GO', 'bid': '113.576', 'ask': '113.590', 'spread': 0.014}
継続-トレード可能
ポジあり。待機
待機中  58

とりあえずは売買シグナルが発生したら注文を行い約定後は何もせず待機するという感じで動いています。

{'status': 'SKIP', 'bid': '113.957', 'ask': '113.980', 'spread': 0.023}
スキップ-スプレッド拡大中
待機中  1195
{'status': 'SKIP', 'bid': '113.952', 'ask': '113.978', 'spread': 0.026}
スキップ-スプレッド拡大中
待機中  1196
{'status': 'SKIP', 'bid': '113.966', 'ask': '113.999', 'spread': 0.033}
スキップ-スプレッド拡大中
待機中  1197
{'status': 'SKIP', 'bid': '113.970', 'ask': '114.034', 'spread': 0.064}
スキップ-スプレッド拡大中
待機中  1198
{'status': 'SKIP', 'bid': '113.979', 'ask': '114.023', 'spread': 0.044}
スキップ-スプレッド拡大中
待機中  1199
{'status': 'SKIP', 'bid': '113.997', 'ask': '114.062', 'spread': 0.065}
スキップ-スプレッド拡大中
待機中  1200
{'status': 'STOP', 'bid': '113.854', 'ask': '113.954', 'spread': 0.1}
停止ーマーケットクローズ
Botが停止しました。 UTC: 2021-10-29 20:59:58.850593+00:00

また金曜日(NY時間の午後5時直前)に、スプレッドが広がった後マーケットクローズによってボットが停止した事を確認できました。


まとめ


利確損切注文の値幅の設定の部分ですが、初めてのボットなので固定値で値幅を指定でもよかったのですが、ちょっとこれから本格的なボットに変身していくぞ感が欲しかったのでこのような感じにしてみました。 これで発注処理まで終わりましたので、とりあえずな感じのボットが完成したことになります。

とりあえず完成しただけですので、まずは安定稼働させることが第一の目標になります。ですのでこのボットを実際に動かしてみて取引の結果よりもどんなエラーが発生するのか確認しておきたいと思います。
とは言えテストをするのにまずこのボットを24時間動かし続けられる環境が必要になりますので、やはりとりあえずの感じで24時間可動な普段使いでないPCで疑似クラウドサーバー的な感じでボットを実行してみます。

安定稼働が見込めるようになったら、もう少し細かいエラー処理(エラーの通知やリトライ処理)や資金管理処理を追加していく予定です。
いつになることやらですが、続けていけるように頑張っていきたいと思います。